# -*- coding: utf-8 -*-
#
import struct
from abc import ABCMeta, abstractproperty
from collections import namedtuple
from datetime import datetime
from StringIO import StringIO

__all__ = ['Database',
    'Contact', 'User', 'Event', 'Module', 'Settings', 'Header',
    'DBEvent', 'DBModuleName', 'DBContact', 'DBHeader',
    'MessageEvent', 'URLEvent', 'ContactEvent', 'AddedEvent',
    'AuthRequestEvent', 'FileEvent', 'StatusChangeEvent', 'SMTPSimpleEvent',
    'NicknameChangeEvent', 'StatusMessageChangeEvent', 'AvatarEvent',
    'ContactLeftChannelEvent', 'VoiceCallEvent', 'MessageEvent',
    'URLEvent', 'ContactEvent', 'AddedEvent', 'AuthRequestEvent',
    'FileEvent', 'StatusChangeEvent', 'SMTPSimpleEvent',
    'NicknameChangeEvent', 'StatusMessageChangeEvent', 'AvatarEvent',
    'ContactLeftChannelEvent', 'VoiceCallEvent'
]

################################################################################
# Structs
################################################################################

DBHeader = namedtuple('DBHeader',[
    'signature',            # BYTE[16] #  'Miranda ICQ DB',0,26

    'version',              # DWORD # as 4 bytes, ie 1.2.3.10=0x0102030a
                            # this version is 0x00000700

    'ofsFileEnd',           # DWORD # offset of the end of the database - place
                            # to write new structures

    'slackSpace',           # DWORD # a counter of the number of bytes that
                            # have been wasted so far due to deleting structures
                            # and/or e-making them at the end. We should compact
                            # when this gets above a threshold

    'contactCount',         # DWORD # number of contacts in the chain,excluding
                            #the user

    'ofsFirstContact',      # DWORD # offset to first struct DBContact
                            # in the chain

    'ofsUser',              # DWORD # offset to struct DBContact representing
                            # the user

    'ofsFirstModuleName'    # DWORD # offset to first struct DBModuleName
                            # in the chain
])

DBContact = namedtuple('DBContact', [
    'signature',            # DWORD

    'ofsNext',              # DWORD # offset to the next contact in the chain.
                            # zero if this is the 'user' contact or the last
                            # contact in the chain

    'ofsFirstSettings',     # DWORD # offset to the first DBContactSettings
                            # in the chain for this contact.

    'eventCount',           # DWORD # number of events in the chain for
                            # this contact

    'ofsFirstEvent',        # DWORD # offsets to the first DBEvent in
                            # the chain for this contact

    'ofsLastEvent',         # DWORD # offsets to the last DBEvent in
                            # the chain for this contact

    'ofsFirstUnreadEvent',  # DWORD # offset to the first (chronological)
                            # unread even in the chain, 0 if all are read

    'timestampFirstUnread'  # DWORD #timestamp of the event at
                            # ofsFirstUnreadEvent
])

DBModuleName = namedtuple('DBModuleName', [
    'signature',            # DWORD

    'ofsNext',              # DWORD # offset to the next module name
                            # in the chain

    'cbName',               # BYTE # number of characters in this module name

    'name',                 # char[1] # name, no nul terminator
])

DBContactSettings = namedtuple('DBContactSettings', [
    'signature',            # DWORD

    'ofsNext',              # DWORD # offset to the next contactsettings
                            # in the chain

    'ofsModuleName',        # DWORD # offset to the DBModuleName of the owner
                            # of these settings

    'cbBlob',               # DWORD # size of the blob in bytes. May be larger
                            # than the actual size for reducing the number of
                            # moves required using granularity in resizing

    'blob',                 # BYTE[1] # the blob. a back-to-back sequence of
                            # DBSetting structs, the last has cbName=0
])

DBEvent = namedtuple('DBEvent', [
    'signature',            # DWORD

    'ofsPrev',              # DWORD # offset to the previous event in the chain.
                            # Chain is sorted chronologically

    'ofsNext',              # DWORD # offset to the next event in the chain.
                            # Chain is sorted chronologically

    'ofsModuleName',        # DWORD # offset to a DBModuleName struct of the
                            # name of the owner of this event

    'timestamp',            # DWORD # seconds since 00:00:00 01/01/1970

    'flags',                # DWORD # see m_database.h, db/event/add

    'eventType',            # WORD # module-defined event type

    'cbBlob',               # DWORD # number of bytes in the blob

    'blob',                 # BYTE # the blob. module-defined formatting
])

################################################################################
# Helpers
################################################################################

read_string = lambda f,s: struct.unpack('={0}s'.format(s), f.read(s))[0]
read_dword = lambda f: struct.unpack('i', f.read(4))[0]
read_word = lambda f: struct.unpack('h', f.read(2))[0]
read_char = lambda f, s: struct.unpack('={0}c'.format(s), f.read(s))
read_byte = lambda f: struct.unpack('b', f.read(1))[0]
read_ubyte = lambda f: struct.unpack('B', f.read(1))[0]

def cached(func, _cache={}):
    '''Stores objects by their offset'''
    def wrapper(cls, file, offset):
        obj = _cache.get(offset)
        if obj is None:
            return func(cls, file, offset)
        return obj
    return wrapper

class ABCStruct(object):
    __metaclass__ = ABCMeta

    def __init__(self, file, offset):
        if self.signature != self.valid_signature:
            raise ValueError(
                ('Invalid object signature.'
                    + 'Got: 0x{0.signature:0x} Valid: 0x{0.valid_signature:0x}'
                    if isinstance(self.signature, int) else
                    'Got: {0.signature} Valid: {0.valid_signature}'
                ).format(self)
            )
        self._file = file
        self._offset = offset

    @abstractproperty
    def valid_signature(self):
        '''Must contains signature to make sure, that we've got the right data'''
        raise NotImplementedError()

################################################################################
# Header
################################################################################

class Header(DBHeader, ABCStruct):
    '''Represents DBHeader structure'''

    @cached
    def __new__(cls, file, offset):
        file.seek(offset, 0)
        return DBHeader.__new__(cls, *[
            read_string(file, 16),  # signature
            read_dword(file),       # version
            read_dword(file),       # ofsFileEnd
            read_dword(file),       # slackSpace
            read_dword(file),       # contactCount
            read_dword(file),       # ofsFirstContact
            read_dword(file),       # ofsUser
            read_dword(file),       # ofsFirstModule
        ])

    @property
    def valid_signature(self):
        '''see http://miranda.googlecode.com/svn/trunk/miranda/plugins/db3x_mmap/database.h'''
        return 'Miranda ICQ DB\x00\x1A'

    def get_first_contact(self):
        '''Retrieve first contact from database.

        Returns:
            Contact object if it exists, otherwise None will be returned.
        '''
        return Contact(self._file, self.ofsFirstContact)

    def get_first_module(self):
        '''Retrieve first module from database.

        Returns:
            Module object if it exists, otherwise None will be returned.
        '''
        return Module(self._file, self.ofsFirstModuleName)

    def get_user(self):
        '''Retrieve user information from database.

        Returns:
            User object if it exists, otherwise None will be returned.
        '''
        return User(self._file, self.ofsUser)

################################################################################
# Contact
################################################################################

class Contact(DBContact, ABCStruct):
    '''Represents DBContact structure'''

    @cached
    def __new__(cls, file, offset):
        if offset == 0:
            return None
        file.seek(offset, 0)
        return DBContact.__new__(cls, *[
            read_dword(file),   # signature
            read_dword(file),   # ofsNext
            read_dword(file),   # ofsFirstSettings
            read_dword(file),   # eventCount
            read_dword(file),   # ofsFirstEvent
            read_dword(file),   # ofsLastEvent
            read_dword(file),   # ofsFirstUnreadEvent
            read_dword(file),   # timestampFirstUnread
        ])

    @property
    def valid_signature(self):
        '''see http://miranda.googlecode.com/svn/trunk/miranda/plugins/db3x_mmap/database.h'''
        return int(0x43DECADE)

    def next(self):
        '''Retrieve next contact in chain.

        Returns:
            Contact object or None if no more contacts exists.
        '''
        return Contact(self._file, self.ofsNext)

    def get_first_event(self):
        '''Retrieve first event for this contact.

        Returns:
            Event object or None if no more events exists.
        '''
        return Event(self._file, self.ofsFirstEvent)

    def get_first_unread_event(self):
        '''Retrieve first unread event for this contact.

        Returns:
            Event object or None if no more events exists.
        '''
        return Event(self._file, self.ofsFirstUnreadEvent)

    def get_last_event(self):
        '''Retrieve last event for this contact.

        Returns:
            Event object or None if no more events exists.
        '''
        return Event(self._file, self.ofsLastEvent)

    def get_first_settings(self):
        '''Retrieves first settings in chain for this contact.

        Returns:
            Settings object or None if no settings founded.
        '''
        return Settings(self._file, self.ofsFirstSettings)

    @property
    def events(self):
        '''Retrieve all events.

        Yields:
            Event object or None if no more events exists.
        '''
        event = self.get_first_event()
        while True:
            if event is None:
                break
            yield event
            event = event.next()

    @property
    def new_events(self):
        '''Retrieve all unread events for this contact.

        Yields:
            Event object or None if no more events exists.
        '''
        event = self.get_first_unread_event()
        while True:
            if event is None:
                break
            yield event
            event = event.next()

    @property
    def settings(self):
        '''Retrieve all settings for this contact.

        Yields:
            Settings object.
        '''
        settings = self.get_first_settings()
        while True:
            if settings is None:
                break
            yield settings
            settings = settings.next()

    @property
    def datetime_first_unread(self):
        '''Converts timestampFirstUnread property to datetime.

        Returns:
            datetime object based on first unevent timestamp.
        '''
        return datetime.fromtimestamp(self.timestampFirstUnread)

class User(Contact):
    '''Represents database user as Contact object'''

    def next(self):
        '''Raises error due to there could be only one database user.

        Raises:
            ValueError: always.
        '''
        raise ValueError('Miranda IM databases are have no multi player mode')

class Module(DBModuleName, ABCStruct):
    '''Represents DBModuleName structure'''

    @cached
    def __new__(cls, file, offset):
        if offset == 0:
            return None
        file.seek(offset, 0)
        data = [
            read_dword(file),   # signature
            read_dword(file),   # ofsNext
            read_byte(file),    # cbName
        ]
        data.append(read_char(file, data[-1]))  # name
        return DBModuleName.__new__(cls, *data)

    @property
    def valid_signature(self):
        '''see http://miranda.googlecode.com/svn/trunk/miranda/plugins/db3x_mmap/database.h'''
        return int(0x4DDECADE)

    @property
    def name(self):
        '''Returns module name as string, not as sequence of bytes.

        Return:
            String with module name.
        '''
        return ''.join(DBModuleName.name.fget(self))

    def next(self):
        '''Retrieve next module in chain.

        Returns:
            Module object or None if this is the last one.
        '''
        return Module(self._file, self.ofsNext)

################################################################################
# Settings
################################################################################

class Settings(DBContactSettings, ABCStruct):
    '''Represents DBContactSettings structure'''

    @cached
    def __new__(cls, file, offset):
        if offset == 0:
            return None
        file.seek(offset, 0)
        data = [
            read_dword(file),   # signature
            read_dword(file),   # ofsNext
            read_dword(file),   # ofsModuleName
            read_dword(file),   # cbBlob
        ]
        data.append(read_string(file, data[-1]))  # blob
        return DBContactSettings.__new__(cls, *data)

    def __getitem__(self, key):
        if isinstance(key, int):
            return super(Settings, self).__getitem__(key)
        return self.data[key]

    @property
    def valid_signature(self):
        '''see http://miranda.googlecode.com/svn/trunk/miranda/plugins/db3x_mmap/database.h'''
        return int(0x53DECADE)

    @property
    def module(self):
        '''Retrieve owner module for this setting.

        Returns:
            Module object
        '''
        return Module(self._file, self.ofsModuleName)

    def next(self):
        '''Retrieve next settings in chain.

        Returns:
            Settings object or None if no more settings exists.
        '''
        return Settings(self._file, self.ofsNext)

    @property
    def data(self, _cached={}):
        '''Returns settings data as dict.

        Returns:
            dict with stored settings.
        '''
        if self._offset in _cached:
            return _cached[self._offset]
        data = {}
        stream = StringIO(self.blob)
        while True:
            key = read_string(stream, read_ubyte(stream))
            if not key.strip('\x00'):
                break
            type = read_ubyte(stream)
            if type == 0: # deleted
                continue
            elif type == 1:
                value = read_byte(stream)
            elif type == 2:
                value = read_word(stream)
            elif type == 4:
                value = read_dword(stream)
            else:
                value = read_string(stream, read_ubyte(stream) + 1).strip('\x00')
            data[key] = value
            if stream.tell() >= len(self.blob):
                break
        _cached[self._offset] = data
        return data

    def get(self, key, default=None):
        '''Get settings item by key.

        Args:
            key: Name of extracted item
            default: Default value if no item found.

        Returns:
            Item value by specified key or default value if nothing was founded.
        '''
        return self.data.get(key, default)

################################################################################
# Events
################################################################################

class Event(DBEvent, ABCStruct):
    '''Represents DBEvent structure and abstract Event object

    If defined some Event subclass with valid_type property, then will be
    returned his instance instead of Event one if eventType and valid_type
    properties are matched. Otherwise, Event instance will be returned as
    expected.

    For example: when Event instance will be tried to be created from data
    which has eventType 0, then instance of MessageEvent will be returned.
    '''

    @cached
    def __new__(cls, file, offset):
        if offset == 0:
            return None
        file.seek(offset, 0)
        data = [
            read_dword(file),   # signature
            read_dword(file),   # ofsNext
            read_dword(file),   # ofsPrev
            read_dword(file),   # ofsModuleName
            read_dword(file),   # timestamp
            read_dword(file),   # flags
            read_word(file),    # eventType
            read_dword(file),   # cbBlob
        ]
        data.append(read_string(file, data[-1]))  # blob
        for child in cls.__subclasses__():
            if child.valid_type.fget(cls) == data[-3]:
                return DBEvent.__new__(child, *data)
        return DBEvent.__new__(cls, *data)

    def __init__(self, *args, **kwargs):
        valid_type = getattr(self, 'valid_type', None)
        if not isinstance(self, Event) and self.eventType != valid_type:
            msg = 'Invalid event type for {0.__name__}. Expected {1} got {2}'
            raise ValueError(msg.format(type(self), self.eventType, valid_type))
        super(Event, self).__init__(*args, **kwargs)

    @property
    def valid_signature(self):
        '''see http://miranda.googlecode.com/svn/trunk/miranda/plugins/db3x_mmap/database.h'''
        return int(0x45DECADE)

    def next(self):
        '''Retrieve next event in chain.

        Returns:
            Event object or None if this is the last event in chain.
        '''
        return Event(self._file, self.ofsNext)

    def prev(self):
        '''Retrieve previous event in chain.

        Returns:
            Event object or None if this is the first event in chain.
        '''
        return Event(self._file, self.ofsPrev)

    @property
    def module(self):
        '''Retrieve module that is owner of this event.

        Returns:
            Module object or None if event has no owner module.
        '''
        return Module(self._file, self.ofsModuleName)

    @property
    def datetime(self):
        '''Converts timestamp property to datetime.

        Returns:
            datetime object based on event timestamp.
        '''
        return datetime.fromtimestamp(self.timestamp)

    @property
    def data(self):
        '''Return blob data with stripped \x00 chars.

        Returns:
            Event data in more pythonic form.
        '''
        return self.blob.strip('\x00')

class MessageEvent(Event):
    '''Represent event with type code 0'''

    @property
    def valid_type(self):
        return 0

class URLEvent(Event):
    '''Represent event with type code 1'''

    @property
    def valid_type(self):
        return 1

class ContactEvent(Event):
    '''Represent event with type code 2'''

    @property
    def valid_type(self):
        return 2

class AddedEvent(Event):
    '''Represent event with type code 1000'''

    @property
    def valid_type(self):
        return 1000

class AuthRequestEvent(Event):
    '''Represent event with type code 1001'''

    @property
    def valid_type(self):
        return 1001

class FileEvent(Event):
    '''Represent event with type code 1002'''

    @property
    def valid_type(self):
        return 1002

class StatusChangeEvent(Event):
    '''Represent event with type code 25368'''

    @property
    def valid_type(self):
        return 25368

class SMTPSimpleEvent(Event):
    '''Represent event with type code 2350'''

    @property
    def valid_type(self):
        return 2350

class NicknameChangeEvent(Event):
    '''Represent event with type code 9001'''

    @property
    def valid_type(self):
        return 9001

class StatusMessageChangeEvent(Event):
    '''Represent event with type code 9002'''

    @property
    def valid_type(self):
        return 9002

class AvatarEvent(Event):
    '''Represent event with type code 9003'''

    @property
    def valid_type(self):
        return 9003

class ContactLeftChannelEvent(Event):
    '''Represent event with type code 9004'''

    @property
    def valid_type(self):
        return 9004

class VoiceCallEvent(Event):
    '''Represent event with type code 8739'''

    @property
    def valid_type(self):
        return 8739

################################################################################
# Database interfaces
################################################################################

class Database(object):
    '''Represents interface to MirandaIM database'''

    def __init__(self, dbpath):
        self._path = dbpath
        self.header = Header(open(dbpath, 'rb'), 0)

    @property
    def path(self):
        '''Returns dbpath.'''
        return self._path

    @property
    def contacts(self):
        '''Retrieve all contacts from database.

        Yields:
            Contact objects.
        '''
        contact = self.header.get_first_contact()
        while True:
            if contact is None:
                break
            yield contact
            contact = contact.next()

    @property
    def modules(self):
        '''Retrieve all modules from database.

        Yields:
            Module objects.
        '''
        module = self.header.get_first_module()
        while True:
            if module is None:
                break
            yield module
            module = module.next()

    @property
    def user(self):
        '''Retrieve Contact object that is representes user.

        Returns:
            User object.
        '''
        return self.header.get_user()
